<?php
// +----------------------------------------------------------------------
// | INPHP
// | Copyright (c) 2023 https://inphp.cc All rights reserved.
// | Licensed ( https://opensource.org/licenses/MIT )
// | Author: 幺月儿(https://gitee.com/lulanyin) Email: inphp@qq.com
// +----------------------------------------------------------------------
// | 安装应用
// | 因安装应用可能存在超时，所以使用命令运行，如果支行在swoole服务中，可以使用TASK
// +----------------------------------------------------------------------
namespace app\admin\cmd;

use app\admin\http\api\modules;
use GuzzleHttp\Client;
use Inphp\Core\Config;
use Inphp\Core\Db\Redis;
use Inphp\Core\Util\File;
use Inphp\Core\Util\Log;
use Inphp\Core\Util\Str;

class installModule
{
    public int $appid = 0;

    public bool $output = false;

    public function __construct(array $params = [])
    {
        $appid = $params["appid"] ?? 0;
        $appid = !empty($appid) ? Str::trim($appid) : 0;
        $this->appid = is_numeric($appid) && $appid > 0 ? ceil($appid) : 0;
        $this->output = $params["output"] ?? false;
    }

    public function run()
    {
        if ($this->appid <= 0) {
            $this->debug("appid 无效");
            return;
        }
        //为保证严谨，重新检测一轮安装状态
        $appid = $this->appid;
        //是否在安装中
        $this->debug("正在检测...");
        $stateCache = Redis::get("store_appid_{$appid}");
        if (!empty($stateCache)) {
            $this->debug("该应用模块正在安装");
            return;
        }
        //检测系统中是否存在
        $list = (new modules())->list();
        foreach ($list as $item) {
            if ($item["appid"] == $appid) {
                $this->debug("该应用模块已安装");
                return;
            }
        }
        //
        //先从应用商城获取应用信息
        $store = \Inphp\Core\Modules::getModule("admin")->getConfig("store") ?? [];
        if (!$store["enable"]) {
            $this->debug("请先在系统配置中，打开应用商店功能");
            return;
        }
        if (empty($store["url"])) {
            $this->debug("请先在系统配置中，填写应用商店地址");
            return;
        }
        if (stripos($store["url"], "http://") !== 0 && stripos($store["url"], "https://") !== 0) {
            $this->debug("应用商店地址配置错误，地址无效");
            return;
        }
        if (empty($store["key"])) {
            $this->debug("请先在系统配置中，填写应用商店密钥");
            return;
        }
        $this->debug("正在获取应用信息");
        $client = new Client();
        $response = $client->request("GET", $store["url"]."/api/app", [
            "query" => [
                "appid"     => $appid,
                "key"       => $store["key"],
                "release"   => 1
            ]
        ]);
        if ($response->getStatusCode() !== 200) {
            $this->debug("应用商店无响应");
            return;
        }
        $json = $response->getBody()->getContents();
        $json = !empty($json) ? (@json_decode($json, true) ?? []) : [];
        if (empty($json)) {
            $this->debug("应用商店响应数据错误");
            return;
        }
        $json["error"] = $json["error"] ?? 1;
        $json["message"] = $json["message"] ?? null;
        $json["data"] = $json["data"] ?? null;
        if ($json["error"] !== 0 || empty($json["data"])) {
            $this->debug($json["message"] ?? "应用商店中不存在此应用模块");
            return;
        }
        //
        $module = $json["data"];
        $id = $module["id"] ?? null;
        if (empty($id)) {
            $this->debug("应用模块ID无法判断处理");
            return;
        }
        foreach ($list as $item) {
            if ($item["id"] == $id) {
                $this->debug("系统已存在相同命名空间的应用模块");
                return;
            }
        }
        Redis::set("store_appid_{$appid}", 1);
        //开始下载模块，进行安装
        $this->debug("开始下载文件");
        $zip = file_get_contents($store["url"]."/app/download?appid={$appid}&key={$store["key"]}");
        if (empty($zip)) {
            Redis::del("store_appid_{$appid}");
            $this->debug("文件下载为空数据");
            return;
        }
        if (stripos($zip, "error") === 0) {
            Redis::del("store_appid_{$appid}");
            $this->debug("文件下载失败：{$zip}");
            return;
        }
        //先保存到文件
        $md5 = md5($zip);
        if (!$this->checkMD5($md5, $store["url"], $appid, $store["key"])) {
            Redis::del("store_appid_{$appid}");
            $this->debug("文件MD5校验未一致，停止安装");
            return;
        }
        $this->debug("已下载到文件，文件md5: {$md5}，开始保存");
        $attachment = Config::get("define.attachment");
        $tmp_file = $attachment."/apps/{$appid}_v{$module['version']}.zip";
        if (is_file($tmp_file)) {
            @unlink($tmp_file);
            usleep(100);
        }
        if (!File::save($tmp_file, $zip)) {
            Redis::del("store_appid_{$appid}");
            $this->debug("无法保存文件至{$tmp_file}，请检查目录权限");
            return;
        }
        //尝试解压文件至app目录
        $dir = ROOT."/app/{$module["id"]}";
        if (!is_dir($dir)) {
            if (!mkdir($dir, 0777, true)) {
                Redis::del("store_appid_{$appid}");
                $this->debug("无法创建目录: {$dir}，请检查目录权限");
                return;
            }
        }
        //解压过去
        $zip = new \ZipArchive();
        if (!$zip->open($tmp_file)) {
            Redis::del("store_appid_{$appid}");
            $this->debug("无法打开安装包文件：{$tmp_file}");
            return;
        }
        if (!$zip->extractTo($dir)) {
            Redis::del("store_appid_{$appid}");
            $this->debug("无法解压安装包文件：{$tmp_file}");
            return;
        }
        //composer、关联应用
        $require = $module["release"]["require"] ?? [];
        if (!empty($require)) {
            $otherModules = $require["modules"] ?? [];
            if (!empty($otherModules)) {
                foreach ($otherModules as $om) {
                    $omAppid = $om["appid"] ?? 0;
                    $omAppid = is_numeric($omAppid) && $omAppid > 0 ? ceil($omAppid) : 0;
                    $omVersion = $om["version"] ?? 0;
                    $omVersion = is_numeric($omVersion) && $omVersion > 0 ? ceil($omVersion) : 0;
                    if ($omAppid > 0) {
                        $this->debug("正在安装关联应用：{$omAppid}");
                        @CMD_RUN("app.admin.cmd.installModule", [
                            "appid"     => $omAppid,
                            "version"   => $omVersion
                        ], true);
                    }
                }
            }
            /*
            $composer = $require["composer"] ?? [];
            if (!empty($composer)) {
                $bin = is_file(ROOT."/composer.phar") ? (ROOT."/composer.phar") : "composer";
                foreach ($composer as $cps) {
                    $name = $cps["name"] ?? null;
                    $ver = $cps["version"] ?? null;
                    if (!empty($name) && !empty($ver)) {
                        $cmd = "cd ".ROOT." && {$bin} require {$name}:\"{$ver}\"";
                        $this->debug("正在安装composer包：{$name}\r\n{$cmd}");
                        @exec($cmd);
                    }
                }
            }*/
        }
        //安装完成
        $this->debug("安装结束: ".date("Y/m/d H:i:s")."\r\n");
        Redis::del("store_appid_{$appid}");
    }

    private function checkMD5(string $md5, string $baseUrl, int $appid, string $key): bool
    {
        //检测md5是否一致
        $client = new Client();
        $response = $client->request("GET", $baseUrl."/api/app/matchMD5", [
            "query" => [
                "appid"     => $appid,
                "key"       => $key,
                "release"   => 1,
                "md5"       => $md5
            ]
        ]);
        if ($response->getStatusCode() !== 200) {
            $this->debug("应用商店无响应");
            return false;
        }
        $json = $response->getBody()->getContents();
        $json = !empty($json) ? (@json_decode($json, true) ?? []) : [];
        if (empty($json)) {
            $this->debug("应用商店响应数据错误");
            return false;
        }
        $json["error"] = $json["error"] ?? 1;
        $json["message"] = $json["message"] ?? null;
        if ($json["error"] !== 0) {
            $this->debug($json["message"] ?? "应用商店中不存在此应用模块");
            return false;
        }
        return true;
    }

    /**
     * 输出日志
     * @param string $text
     */
    private function debug(string $text)
    {
        $log = Config::get("define.logs")."/installModules/store_appid_{$this->appid}.txt";
        Log::writeToEnd($log, $text);
        if ($this->output) {
            echo $text.PHP_EOL;
        }
    }
}